🔔
Rappel
Ajoutez vos dépenses
G
Ghorab Pro v8.4 - Français
MOUVEMENT FIXED - EUR BASE - PDF
- - - -
GHORB-PRO25-AINML-ILA04-2026Z
zouaoui.ghorab@outlook.fr
📒
المصاريف اليومية - Ghorab Pro v8.4 - Français
🇪🇺 EUR
سبتمبر + الباقي0
المداخيل:0.00 🇪🇺 EUR
الرصيد السابق:0.00 🇪🇺 EUR
المصاريف:0.00 🇪🇺 EUR
الرصيد الحالي:0.00 🇪🇺 EUR
🔒
Mot de passeDéfinissez
☁️
Google Drive Autorisation Google Drive - Ajout email
📷
Caméra Autorisation caméra - Scanner reçus
🔔
NotificationsMode APP
FIXED
ℹ️
À proposInfos application
💶 Taux (en EUR) - Euro en dessus BASE: EUR
EUR = base 1.00 en haut
💱 Cours réel du marchéLIVE
Taux interbancaires temps réel - 1 EUR → autres devises
Cliquez Actualiser pour charger
🌐
Appuyez sur Actualiser pour voir le cours du jour
Version: Ghorab Pro v8.4
Email: zouaoui.ghorab@outlook.fr
📘 Facebook: zouaoui ghorab
𝕏 Twitter: @Zouaoui_Ghorab
Licence:
🔒
🛡️
Question SecrèteNon configurée
📦
Dropbox
Autorisation Dropbox
📧 Email Dropbox
Cet email sera lié à ton compte Dropbox pour l'export auto.
Non connecté
🔑 Pour activer la vraie synchro Google Drive automatique:
1. Va sur console.cloud.google.com
2. Crée un projet > Active Drive API
3. Crée un OAuth Client ID (Web)
4. Colle l'ID ici:
☁️
Google Drive
Autorisation Google Drive
📧 Email Google Drive
Cet email sera lié à ton Drive pour l'export auto.
Non connecté
📒
Dépenses Quotidiennes Ghorab Pro v8.4
Version 9.0 - Ventes & Prêts
zouaoui.ghorab@outlook.fr
📧 Contacter
Activer
Info
PDF prêt
💡 Si rien ne se passe: Cliquez sur ⋯ en haut à droiteOuvrir dans le navigateur / Chrome puis refaites Export PDF
`; const blob=new Blob([full],{type:'text/html'}); if(externalBlobUrl) URL.revokeObjectURL(externalBlobUrl); externalBlobUrl=URL.createObjectURL(blob); // Essaie d'ouvrir - si bloqué on affiche instructions const win=window.open(externalBlobUrl,'_blank'); if(win){ showToast('✅ Rapport ouvert - Cliquez sur IMPRIMER dans la nouvelle page'); }else{ // Popup bloquée - on affiche l'iframe dans la page actuelle document.getElementById('pdfPreview').innerHTML=`
⚠️ Popup bloquée par l'app
Cliquez sur ⋯ → Ouvrir dans le navigateur en haut, puis réessayez
`+full+`
🔗 OUVRIR RAPPORT DANS NAVIGATEUR
`; // On affiche aussi la section print document.getElementById('printSection').innerHTML=full; document.getElementById('printSection').style.display='block'; showToast('⚠️ Popup bloquée - Utilisez Ouvrir dans navigateur (⋯ en haut)'); } // Sauve aussi pour partage lastPDFContent=full; }catch(e){ console.error(e); showToast('❌ Erreur: '+e.message+' - Utilisez Copier tableau'); } } function downloadPDFText(){ // Même action que printPDF car téléchargement bloqué printPDF(); } function openExternal(){ if(externalBlobUrl){ window.open(externalBlobUrl,'_blank'); showToast('🔗 Ouverture externe...'); }else{ printPDF(); setTimeout(()=>{ if(externalBlobUrl) window.open(externalBlobUrl,'_blank'); },500); } } function sharePDF(){ try{ if(navigator.share && lastPDFContent){ const blob=new Blob([lastPDFContent],{type:'text/html'}); const file=new File([blob], 'Ghorab_Rapport.html', {type:'text/html'}); navigator.share({title:'Ghorab Pro Rapport', files:[file]}).catch(()=>copyPDF()); }else copyPDF(); }catch(e){copyPDF();} } function copyPDF(){ try{ let txt='GHORAB PRO v8.3 - RAPPORT\nDate: '+new Date().toLocaleString()+'\n\n'; const disp=document.getElementById('deviseTop').value; txt+='Date | Type | Cat | Montant | Devise | Convert '+disp+'\n'; transactions.forEach(t=>{ const corrType=getCorrectTypeForCat(t.cat); const conv=convert(t.montant,t.devise||'EUR',disp).toFixed(2); txt+=`${t.date} | ${corrType} | ${t.cat} | ${t.montant} | ${t.devise||'EUR'} | ${conv}\n`; }); const ta=document.createElement('textarea');ta.value=txt;document.body.appendChild(ta);ta.select();document.execCommand('copy');document.body.removeChild(ta); showToast('📋 Copié'); }catch(e){showToast('Copie manuelle');} } function exportJSON(){ try{ const data={transactions,comptes,rates,budget,lang,notifSettings,exportDate:new Date().toISOString(),version:'v8.4'}; const content=JSON.stringify(data,null,2); const blob=new Blob([content],{type:'application/json'}); const url=URL.createObjectURL(blob); const a=document.createElement('a');a.href=url;a.download='ghorab_backup_'+new Date().toISOString().slice(0,10)+'.json';document.body.appendChild(a);a.click();setTimeout(()=>{document.body.removeChild(a);URL.revokeObjectURL(url);},1000); showToast('💾 Backup JSON'); }catch(e){showToast('Erreur JSON: '+e.message);} } function openImport(){document.getElementById('fileInput').click();} function handleImport(event){ const file=event.target.files[0];if(!file)return; const reader=new FileReader(); reader.onload=function(e){ try{ const data=JSON.parse(e.target.result); if(!data.transactions){showToast('Fichier invalide');return;} if(!confirm('Importer '+data.transactions.length+' tx ?'))return; transactions=data.transactions||[];comptes=data.comptes||comptes;rates=data.rates||rates;budget=data.budget||budget; localStorage.setItem('ghorab_pro_v4',JSON.stringify(transactions)); localStorage.setItem('ghorab_comptes',JSON.stringify(comptes)); localStorage.setItem('ghorab_rates',JSON.stringify(rates)); localStorage.setItem('ghorab_budget_multi',JSON.stringify(budget)); renderAll();showToast('📥 Importe - '+transactions.length+' tx'); }catch(err){showToast('Erreur import: '+err.message);} }; reader.readAsText(file);event.target.value=''; } // v7.2 ROBUST REAL RATES with 3 fallbacks + CORS proxy let liveRatesCache=null; let liveRatesTime=null; async function fetchRealRates(){ const btn=document.getElementById('btnFetchReal'); const status=document.getElementById('realRateStatus'); const grid=document.getElementById('realRateGrid'); if(btn){btn.innerHTML='⏳ Chargement...'; btn.disabled=true;} if(status) status.innerHTML='🌐 Connexion marché...'; let data=null; const apis=[ 'https://open.er-api.com/v6/latest/EUR', 'https://api.exchangerate-api.com/v4/latest/EUR', 'https://api.frankfurter.app/latest?from=EUR&to=DZD,USD,SAR,TRY,MAD,TND,GBP,EGP,CAD,AED,QAR,KWD,BHD,OMR,JOD,LYD', 'https://api.exchangerate.host/latest?base=EUR&symbols=DZD,USD,SAR,TRY,MAD,TND,GBP,EGP,CAD,AED,XOF,QAR,KWD,BHD,OMR,JOD,LYD,LBP,SYP,IQD,YER,SDG,MRU' ]; for(const url of apis){ try{ console.log('Trying',url); const res=await fetch(url,{mode:'cors', cache:'no-cache'}); if(!res.ok) continue; const j=await res.json(); // Normalize if(j.rates && j.rates.DZD){ // er-api format data={DZD:j.rates.DZD, USD:j.rates.USD, SAR:j.rates.SAR, TRY:j.rates.TRY, MAD:j.rates.MAD||10.8, TND:j.rates.TND||3.3, GBP:j.rates.GBP||0.85, EGP:j.rates.EGP||54, CAD:j.rates.CAD||1.47, AED:j.rates.AED||3.97, XOF:j.rates.XOF||655, QAR:j.rates.QAR||3.96, KWD:j.rates.KWD||0.33, BHD:j.rates.BHD||0.41, OMR:j.rates.OMR||0.42, JOD:j.rates.JOD||0.77, LYD:j.rates.LYD||5.2, LBP:j.rates.LBP||97000, SYP:j.rates.SYP||14000, IQD:j.rates.IQD||1420, YER:j.rates.YER||575, SDG:j.rates.SDG||650, MRU:j.rates.MRU||43, time:j.time_last_update_utc||new Date().toUTCString()}; break; } if(j.rates && j.rates.DZD!==undefined && j.base==='EUR'){ // frankfurter // frankfurter returns rates directly as EUR->X data={DZD:j.rates.DZD, USD:j.rates.USD, SAR:j.rates.SAR, TRY:j.rates.TRY, MAD:j.rates.MAD||10.8, TND:j.rates.TND||3.3, GBP:j.rates.GBP||0.85, EGP:j.rates.EGP||54, CAD:j.rates.CAD||1.47, AED:j.rates.AED||3.97, XOF:j.rates.XOF||655, QAR:j.rates.QAR||3.96, KWD:j.rates.KWD||0.33, BHD:j.rates.BHD||0.41, OMR:j.rates.OMR||0.42, JOD:j.rates.JOD||0.77, LYD:j.rates.LYD||5.2, LBP:j.rates.LBP||97000, SYP:j.rates.SYP||14000, IQD:j.rates.IQD||1420, YER:j.rates.YER||575, SDG:j.rates.SDG||650, MRU:j.rates.MRU||43, time:j.date}; break; } if(j.result==='success' && j.conversion_rates){ // exchangerate-api v4 data={DZD:j.conversion_rates.DZD, USD:j.conversion_rates.USD, SAR:j.conversion_rates.SAR, TRY:j.conversion_rates.TRY, MAD:j.conversion_rates.MAD||10.8, TND:j.conversion_rates.TND||3.3, GBP:j.conversion_rates.GBP||0.85, EGP:j.conversion_rates.EGP||54, CAD:j.conversion_rates.CAD||1.47, AED:j.conversion_rates.AED||3.97, XOF:j.conversion_rates.XOF||655, QAR:j.conversion_rates.QAR||3.96, KWD:j.conversion_rates.KWD||0.33, BHD:j.conversion_rates.BHD||0.41, OMR:j.conversion_rates.OMR||0.42, JOD:j.conversion_rates.JOD||0.77, LYD:j.conversion_rates.LYD||5.2, LBP:j.conversion_rates.LBP||97000, SYP:j.conversion_rates.SYP||14000, IQD:j.conversion_rates.IQD||1420, YER:j.conversion_rates.YER||575, SDG:j.conversion_rates.SDG||650, MRU:j.conversion_rates.MRU||43, time:j.time_last_updated||''}; break; } }catch(e){ console.log('API fail',url,e); continue; } } if(!data){ // Fallback: taux marché approximatif fixe du jour (si pas internet) data={DZD:268.5, USD:1.08, SAR:4.06, TRY:36.2, MAD:10.85, TND:3.35, GBP:0.85, EGP:54.2, CAD:1.47, AED:4.0, XOF:655.9, QAR:3.96, KWD:0.33, BHD:0.41, OMR:0.42, JOD:0.77, LYD:5.2, LBP:97000, SYP:14000, IQD:1420, YER:575, SDG:650, MRU:43, time:'Mode hors ligne - 17/09/2026'}; if(status) status.innerHTML='⚠️ Pas internet - taux estimés affichés'; if(grid) grid.style.border='2px dashed #f59e0b'; } else { if(status) status.innerHTML='✅ Cours réel chargé'; } // Affiche liveRatesCache=data; window._realRates=data; const mapping=data; const timeStr=data.time||new Date().toLocaleString(); const g=document.getElementById('realRateGrid'); if(g){ g.innerHTML=`
🇩🇿 DZD
${mapping.DZD.toFixed(2)}
1 EUR = ${mapping.DZD.toFixed(2)} DZD
Marché réel
🇺🇸 USD
${mapping.USD.toFixed(4)}
1 EUR = ${mapping.USD.toFixed(4)} $
🇸🇦 SAR
${mapping.SAR.toFixed(4)}
1 EUR = ${mapping.SAR.toFixed(4)} ﷼
🇹🇷 TRY
${mapping.TRY.toFixed(4)}
1 EUR = ${mapping.TRY.toFixed(4)} ₺
⏰ ${timeStr}
Source: taux interbancaire live
EUR
`; setTimeout(()=>{convertRealAmount();},200); } const btnApply=document.getElementById('btnApplyReal'); if(btnApply) btnApply.style.display='block'; const dataBlock=document.getElementById('realRatesData'); if(dataBlock) dataBlock.style.display='none'; if(btn){btn.innerHTML='🔄 Actualiser cours réel'; btn.disabled=false;} if(data){ showToast('✅ Cours réel chargé: 1 EUR = '+data.DZD.toFixed(2)+' DZD'); if(window._realRates){ try{ applyRealRatesToApp(); showToast('✅ Taux appliqués auto'); }catch(e){} } } } function convertRealAmount(){ const amt=parseFloat(document.getElementById('convRealAmount')?.value)||0; const out=document.getElementById('convRealResult'); if(!out || !window._realRates || amt<=0){ if(out) out.innerHTML=''; return;} let h=''; const flags={DZD:'🇩🇿',USD:'🇺🇸',SAR:'🇸🇦',TRY:'🇹🇷',MAD:'🇲🇦',TND:'🇹🇳',GBP:'🇬🇧',EGP:'🇪🇬',XOF:'🇸🇳',CAD:'🇨🇦',AED:'🇦🇪',QAR:'🇶🇦',KWD:'🇰🇼',BHD:'🇧🇭',OMR:'🇴🇲',JOD:'🇯🇴',LYD:'🇱🇾',LBP:'🇱🇧',SYP:'🇸🇾',IQD:'🇮🇶',YER:'🇾🇪',SDG:'🇸🇩',MRU:'🇲🇷'}; const syms={DZD:'دج',USD:'$',SAR:'﷼',TRY:'₺',MAD:'د.م',TND:'د.ت',GBP:'£',EGP:'ج.م',XOF:'CFA',CAD:'C$',AED:'د.إ',QAR:'ر.ق',KWD:'د.ك',BHD:'د.ب',OMR:'ر.ع',JOD:'د.أ',LYD:'د.ل',LBP:'ل.ل',SYP:'ل.س',IQD:'د.ع',YER:'﷼',SDG:'ج.س',MRU:'أ.م'}; for(const [code,val] of Object.entries(window._realRates)){ if(code==='time') continue; h+=`
${flags[code]||''} ${code}${(amt*val).toFixed(2)} ${syms[code]||''}
`; } out.innerHTML=h; } function applyRealRatesToApp(){ if(!window._realRates){showToast('❌ Chargez d\'abord le cours');return;} rates['DZD']= 1 / window._realRates.DZD; rates['USD']= 1 / window._realRates.USD; rates['SAR']= 1 / window._realRates.SAR; rates['TRY']= 1 / window._realRates.TRY; rates['MAD']= 1 / window._realRates.MAD; rates['TND']= 1 / window._realRates.TND; rates['GBP']= 1 / window._realRates.GBP; rates['EGP']= 1 / window._realRates.EGP; rates['XOF']= 1 / window._realRates.XOF; rates['CAD']= 1 / window._realRates.CAD; rates['AED']= 1 / window._realRates.AED; rates['QAR']= 1 / window._realRates.QAR; rates['KWD']= 1 / window._realRates.KWD; rates['BHD']= 1 / window._realRates.BHD; rates['OMR']= 1 / window._realRates.OMR; rates['JOD']= 1 / window._realRates.JOD; rates['LYD']= 1 / window._realRates.LYD; rates['LBP']= 1 / window._realRates.LBP; rates['SYP']= 1 / window._realRates.SYP; rates['IQD']= 1 / window._realRates.IQD; rates['YER']= 1 / window._realRates.YER; rates['SDG']= 1 / window._realRates.SDG; rates['MRU']= 1 / window._realRates.MRU; rates['XOF']= 1 / window._realRates.XOF; rates['EUR']=1.0; localStorage.setItem('ghorab_rates',JSON.stringify(rates)); if(typeof renderRatesEditor==='function') renderRatesEditor(); if(typeof updateLiveConverter==='function') updateLiveConverter(); if(typeof renderAll==='function') renderAll(); showToast('✅ Taux réels appliqués'); } function updatePurgeBtn(){ /* v7.4 bouton supprimé */ } function renderRatesEditor(){ let html=''; html+=`
🇪🇺 EUR ⭐EUR (BASE)
`; CURRENCIES.filter(c=>c.code!=='EUR').forEach(c=>{ const val=rates[c.code]!==undefined?rates[c.code]:c.rate; html+=`
${c.flag} ${c.code}${c.symbol}
= ${c.code} en EUR
`; }); document.getElementById('ratesEditor').innerHTML=html; updateRatesPreview(); CURRENCIES.filter(c=>c.code!=='EUR').forEach(c=>{ const el=document.getElementById('rate_'+c.code); if(el){el.addEventListener('input',updateRatesPreview);} }); } function updateRatesPreview(){ let preview=''; try{ CURRENCIES.filter(c=>c.code!=='EUR').forEach(c=>{ const el=document.getElementById('rate_'+c.code); let v=el?el.value.replace(',','.'):rates[c.code]; v=parseFloat(v); if(!isNaN(v)&&v>0){ const inv=(1/v).toFixed(2); preview+=`1 EUR = ${inv} ${c.code} | 1 ${c.code} = ${v} EUR
`; } }); }catch(e){preview='Erreur';} document.getElementById('ratesPreview').innerHTML=DICT[lang].ratesPreview+'
'+preview; } function saveRates(){ try{ let hasError=false; let hasHuge=false; CURRENCIES.filter(c=>c.code!=='EUR').forEach(c=>{ const el=document.getElementById('rate_'+c.code); if(!el) return; let raw=el.value.trim().replace(',','.'); let v=parseFloat(raw); if(isNaN(v)||v<=0){hasError=true;el.style.border='2px solid #ef4444';} else if(v>10){ // Si user tape 1000 dans DZD, c'est probablement un montant pas un taux // On auto-corrige: 1000 DZD = 1 EUR => taux = 1/1000 =0.001 mais on garde 0.004 par défaut? On avertit hasHuge=true; el.style.border='2px solid #f59e0b'; }else{el.style.border='2px solid #000';rates[c.code]=v;} }); if(hasError){showToast('❌ Vérifiez les nombres >0');return;} if(hasHuge){ showToast('⚠️ Valeur >10 détectée (ex: DZD 1000) - C\'est un montant pas un taux! Utilisez le convertisseur vert au dessus, pas les cases taux. Taux DZD doit être ~0.004'); // Ne sauvegarde pas les énormes return; } rates['EUR']=1.0; localStorage.setItem('ghorab_rates',JSON.stringify(rates)); showToast('✅ Taux en EUR sauvés'); updateLiveConverter(); updateRatePreview(); renderAll(); }catch(e){showToast('Erreur: '+e.message);} } function resetRates(){ // Modal custom au lieu de confirm bloqué const modalHtml = ``; document.body.insertAdjacentHTML('beforeend', modalHtml); } function confirmResetRates(){ const m=document.getElementById('resetRateModal'); if(m) m.remove(); rates=Object.assign({},DEFAULT_RATES_EUR); localStorage.setItem('ghorab_rates',JSON.stringify(rates)); renderRatesEditor(); // reset convertisseur aussi const convAmt=document.getElementById('convAmount'); if(convAmt){ convAmt.value=100; } setTimeout(()=>{ updateLiveConverter(); },300); showToast('🔄 Taux réinitialisés par défaut'); renderAll(); } function convert(m,from,to){ if(from===to) return m; const rf=rates[from]!==undefined?rates[from]:1; const rt=rates[to]!==undefined?rates[to]:1; return m*rf/rt; } function safeSet(id, val){ try{ var el=document.getElementById(id); if(el) el.textContent=val; }catch(e){} } function applyLang(){ try{ // v8.4 FIX - force translate comptes selects on language change if(typeof comptes !== 'undefined' && comptes.length>0){ const top=document.getElementById('compteTop'); if(top){ const cur=top.value; const dTmp = (typeof DICT !== 'undefined' && DICT[lang]) ? DICT[lang] : {tous:'All'}; top.innerHTML=''+comptes.map(c=>``).join(''); if(comptes.includes(cur)||cur==='all') top.value=cur; } const sel=document.getElementById('compteSel'); if(sel){ const cur2=sel.value; sel.innerHTML=comptes.map(c=>``).join(''); if(cur2) sel.value=cur2; } } }catch(e){ console.log('fix compte lang error',e); } const d=DICT[lang]; document.documentElement.lang=lang; document.documentElement.dir=lang==='ar'?'rtl':'ltr'; document.body.style.direction=lang==='ar'?'rtl':'ltr'; document.getElementById('appTitle').textContent=d.appTitle; document.getElementById('licTitle').textContent=d.licTitle; document.getElementById('btnDemo').textContent=d.btnDemo; document.getElementById('btnAct').textContent=d.btnAct; document.getElementById('lblRev').textContent=d.rev; document.getElementById('lblPrev').textContent=d.prev; document.getElementById('lblDep').textContent=d.dep; document.getElementById('lblSolde').textContent=d.solde; document.getElementById('searchMouv').placeholder=d.search; document.getElementById('titleMouv').textContent=d.titleMouv; document.getElementById('titleDate').textContent=d.titleDate; document.getElementById('titleCat').textContent=d.titleCat; document.getElementById('titleComptes').textContent=d.titleComptes; document.getElementById('titleBudget').textContent=d.titleBudget; document.getElementById('titleParams').textContent=d.titleParams; document.getElementById('titleFreq').textContent=d.titleFreq; document.getElementById('btnFilter').textContent=d.filter; document.getElementById('btnCancel').textContent=d.cancel; document.getElementById('btnSave').textContent=d.save; document.getElementById('btnAddCompte').textContent=d.add; document.getElementById('btnSaveBudget').textContent=d.save; document.getElementById('btnSaveRates').textContent=d.saveRates; document.getElementById('btnResetRates').textContent=d.resetRates; document.getElementById('lblRates').textContent=d.rates; document.getElementById('ratesExplain').textContent=d.ratesExplain; document.getElementById('btnErase').textContent=d.erase; document.getElementById('btnExportPDF').textContent=d.exportPDF; document.getElementById('btnExportJSON').textContent=d.exportJSON; document.getElementById('btnImportJSON').textContent=d.importJSON; safeSet('lblVersion', d.version); safeSet('lblEmail', d.email); safeSet('lblLicense', d.license); safeSet('lblAccounts', d.accounts); safeSet('lblRatesInfo', d.currs); document.getElementById('lblPassTitle').textContent=d.passTitle; document.getElementById('lblPassDesc').textContent=d.passDesc; document.getElementById('lblNotifTitle').textContent=d.notifTitle; document.getElementById('lblNotifDesc').textContent=d.notifDesc; document.getElementById('lblDropDesc').textContent=d.dropDesc; document.getElementById('lblGDriveTitle').textContent=d.gdriveTitle; document.getElementById('lblGDriveDesc').textContent=d.gdriveDesc; document.getElementById('passAppTitle').textContent=d.passScreenTitle; document.getElementById('passLabel').textContent=d.passLabel; document.getElementById('passBtn').textContent=d.passBtn; document.getElementById('passRecover').textContent=d.passRecover; document.getElementById('titlePassPage').textContent=d.titlePass; document.getElementById('titleNotifPage').textContent=d.titleNotif; document.getElementById('titleGDrivePage').textContent=d.titleGDrive; document.getElementById('lblGDrivePageTitle').textContent=d.titleGDrive; document.getElementById('lblNotifEnable').textContent=d.notifEnable; document.getElementById('lblNotifTime').textContent=d.notifTime; document.getElementById('btnSaveNotif').textContent=d.saveNotif; document.getElementById('btnTestNotif').textContent=d.testNotif; document.getElementById('notifInfo').textContent=d.notifInfo; document.getElementById('btnSavePass').textContent=d.savePass; document.getElementById('btnRemovePass').textContent=d.removePass; try{ safeSet('lblSecretTitle', d.secretTitle); safeSet('lblAboutTitle', d.aboutTitle); safeSet('lblAboutDesc', d.aboutDesc); safeSet('titleAboutPage', d.titleAbout); const at1=document.getElementById('aboutText1'); if(at1) at1.innerHTML=d.aboutText1; safeSet('aboutTransTitle', d.aboutTransTitle); const atl=document.getElementById('aboutTransList'); if(atl) atl.innerHTML=d.aboutTransList; safeSet('aboutPrivacyTitle', d.aboutPrivacyTitle); const apt=document.getElementById('aboutPrivacyText'); if(apt) apt.innerHTML=d.aboutPrivacyText; safeSet('aboutDev', d.aboutDev); safeSet('lblSecretBannerTitle', '🔐 '+d.secretBannerTitle); safeSet('lblSecretBadge', d.secretBannerBadge); safeSet('lblSecretChoose', d.secretChooseLabel); const inp=document.getElementById('secretASingle'); if(inp) inp.placeholder=d.secretAnswerPh; const btn=document.getElementById('btnSaveSecret'); if(btn) btn.textContent=d.secretSaveBtn; safeSet('aboutAppName', d.aboutAppName); safeSet('aboutVersion', d.aboutVersion); refreshSecretQuestionsOptions(); updateSecretBadge(); }catch(e){} document.getElementById('btnDropConnect').textContent=d.dropConnect; document.getElementById('btnGDriveConnect').textContent=d.gdriveConnect; document.getElementById('popupLater').textContent=d.later; document.getElementById('popupAdd').textContent=d.addBtn; document.getElementById('montant').placeholder=d.placeholderAmount; document.getElementById('noteTx').placeholder=d.placeholderNote; document.getElementById('budgetInput').placeholder=d.placeholderBudget; document.getElementById('newCompteName').placeholder=d.placeholderNewAccount; document.getElementById('newPass').placeholder=d.newPassPh; document.getElementById('confirmPass').placeholder=d.confirmPh; document.getElementById('fAll').textContent=d.all; document.getElementById('fIn').textContent=d.in; document.getElementById('fOut').textContent=d.out; document.querySelectorAll('.backBtn').forEach(b=>b.textContent=d.back); document.getElementById('langBtn').textContent= lang==='fr'?'🇫🇷 FR': lang==='en'?'🇬🇧 EN':'🇩🇿 AR'; const devOptions=CURRENCIES.map(c=>``).join(''); document.getElementById('deviseTop').innerHTML=devOptions; document.getElementById('deviseSel').innerHTML=devOptions; document.getElementById('budgetDevise').innerHTML=CURRENCIES.map(c=>``).join(''); document.getElementById('deviseTop').value=localStorage.getItem('ghorab_display_devise')||'EUR'; const cats=[d.catFood,d.catTrans,d.catLoyer,d.catFact,d.catElec,d.catSalaire,d.catSoins,d.catAutre,d.catVente,d.catPret].filter(Boolean); document.getElementById('categorie').innerHTML=cats.map(c=>``).join(''); const gridData=[ {ico:'🪙',lab:d.addRev,act:"openAdd('Rentrée')"}, {ico:'💸',lab:d.addDep,act:"openAdd('Dépense')"}, {ico:'📝',lab:d.mouv,act:"openPage('pageMouv')"}, {ico:'📊',lab:d.repDate,act:"openPage('pageDate')"}, {ico:'📅',lab:d.cal,act:"openPage('pageCal')"}, {ico:'🥧',lab:d.repCat,act:"openPage('pageCat')"}, {ico:'🎯',lab:d.budget,act:"openPage('pageBudget')"}, {ico:'🏦',lab:d.comptes,act:"openPage('pageComptes')"}, {ico:'🗓️',lab:d.freq,act:"openPage('pageFreq')"}, {ico:'⚙️',lab:d.params,act:"openPage('pageParams')"}, ]; document.getElementById('mainGrid').innerHTML=gridData.map(b=>``).join(''); renderAll(); } function toggleLang(){ if(lang==='fr') lang='en'; else if(lang==='en') lang='ar'; else lang='fr'; localStorage.setItem('ghorab_lang',lang); applyLang(); } function fillDemo(){const k="DEM03-PRO25-AINML-ILA04-2026Z".split('-'); function renderFreq(){ const disp=document.getElementById('deviseTop').value; const f=getFiltered(); const d=DICT[lang]; if(f.length===0){ document.getElementById('resFreq').innerHTML=`
🗓️
${d.noMouv}
Ajoutez au moins 2 dépenses avec même catégorie pour voir les fréquentes
💡 Comment ça marche:
• Loyer chaque mois
• Transport chaque jour
• Salaire chaque mois
L'app détecte auto les catégories répétées
`; return; } const grouped={}; f.forEach(t=>{ const key=t.cat+'|'+(t.devise||'EUR'); if(!grouped[key]) grouped[key]={cat:t.cat,devise:t.devise||'EUR',count:0,total:0,last:t.date,lastAmount:t.montant,compte:t.compte,type:t.type}; grouped[key].count++; grouped[key].total+=convert(t.montant,t.devise||'EUR',disp); if(t.date>grouped[key].last){grouped[key].last=t.date; grouped[key].lastAmount=t.montant; grouped[key].compte=t.compte;} }); const list=Object.values(grouped).sort((a,b)=>b.count-a.count); const frequent=list.filter(x=>x.count>=2); const toShow=frequent.length>0?frequent:list; let html=`
⭐ ${frequent.length} catégories fréquentes détectées (utilisées ≥2 fois) sur ${list.length} catégories
`; html+=toShow.map(g=>{ const curr=getCurr(g.devise); const isIn=g.type==='Rentrée'; const color=isIn?'#4ade80':'#f87171'; const avg=(g.total/g.count).toFixed(2); return `
${g.cat}${curr.flag} ${g.devise}${g.count}x
Dernier: ${g.last} • ${g.compte}
Total: ${g.total.toFixed(2)} ${fmtCurr(disp)} • Moy: ${avg} ${fmtCurr(disp)}
`; }).join(''); html+=`
💡 Cliquez sur + montant pour ajouter vite la même dépense
Exemple: Loyer 2000 EUR chaque mois en 1 clic
`; document.getElementById('resFreq').innerHTML=html; } function quickAddFreq(cat,devise,montant,compte){ closePage('pageFreq'); document.getElementById('categorie').value=cat; document.getElementById('deviseSel').value=devise; document.getElementById('montant').value=montant; document.getElementById('compteSel').value=compte; document.getElementById('dateTx').valueAsDate=new Date(); const corr=getCorrectTypeForCat(cat); currentType=corr; const d=DICT[lang]; document.getElementById('modalAddTitle').textContent=corr==='Rentrée'?d.modalAddRev:d.modalAddDep; document.getElementById('modalAdd').classList.add('open'); showToast('⚡ '+cat+' '+montant+' '+devise+' prêt'); } function filterCatFreq(cat){ closePage('pageFreq'); document.getElementById('searchMouv').value=cat; openPage('pageMouv'); setTimeout(()=>renderMouv(),100); showToast('🔍 Filtre: '+cat); } ['key1','key2','key3','key4','key5'].forEach((id,i)=>document.getElementById(id).value=k[i]||'');} function activate(){const key=['key1','key2','key3','key4','key5'].map(id=>document.getElementById(id).value.trim().toUpperCase()).join('-');if(key.length<20){showErr('Clé incomplète');return}const exp=new Date();exp.setDate(exp.getDate()+(key.startsWith('D')?3:365));localStorage.setItem('ghorab_license_v4',key);localStorage.setItem('ghorab_expire_v4',exp.toISOString());document.getElementById('licenseScreen').style.display='none';checkPassScreen();renderAll();} function showErr(m){const e=document.getElementById('licenseError');e.textContent=m;e.style.display='block'} function checkLicense(){const k=localStorage.getItem('ghorab_license_v4');const exp=localStorage.getItem('ghorab_expire_v4');if(k&&exp&&new Date(exp)>new Date()){document.getElementById('licenseScreen').style.display='none';checkPassScreen();}else{document.getElementById('licenseScreen').style.display='grid';}} function checkPassScreen(){const pwd=localStorage.getItem('ghorab_app_password');if(pwd){document.getElementById('passwordScreen').style.display='grid';}else{document.getElementById('passwordScreen').style.display='none';}} function checkPass(){const input=document.getElementById('passInput').value;const stored=localStorage.getItem('ghorab_app_password');if(input===stored){document.getElementById('passwordScreen').style.display='none';}else{alert(lang==='ar'?'خاطئة':'Incorrect');document.getElementById('passInput').value='';}} function openAddVenteFromBanner(){ // Open modal for sales openAdd('Rentrée'); setTimeout(()=>{ const catSel=document.getElementById('catTx'); if(catSel){ const d=DICT[lang]; const target=d.catVente||'Ventes'; // Add option if not exists let found=false; for(let i=0;i{ const catSel=document.getElementById('catTx'); if(catSel){ const d=DICT[lang]; const target=d.catPret||'Prêts'; let found=false; for(let i=0;i{ const cat=(t.cat||'').toLowerCase(); const isVente = cat.includes('vente') || cat.includes('sale') || cat.includes('مبيعات'); const isPret = cat.includes('prêt') || cat.includes('pret') || cat.includes('loan') || cat.includes('قروض') || cat.includes('قرض'); if(isVente){ const c=convert(t.montant,t.devise||'EUR',disp); if(t.type==='Rentrée') totalV+=c; else totalV+=c; countV++; } if(isPret){ const c=convert(t.montant,t.devise||'EUR',disp); totalP+=c; countP++; } }); } const elV=document.getElementById('totalVenteBanner'); const elP=document.getElementById('totalPretBanner'); const elCV=document.getElementById('countVenteBanner'); const elCP=document.getElementById('countPretBanner'); const lblV=document.getElementById('lblBannerVente'); const lblP=document.getElementById('lblBannerPret'); if(elV) elV.textContent=totalV.toFixed(2)+' '+fmtCurr(disp); if(elP) elP.textContent=totalP.toFixed(2)+' '+fmtCurr(disp); if(elCV) elCV.textContent=countV+' '+(d.bannerVenteCount||'ventes'); if(elCP) elCP.textContent=countP+' '+(d.bannerPretCount||'prêts'); if(lblV) lblV.textContent=d.bannerVente||'Ventes'; if(lblP) lblP.textContent=d.bannerPret||'Prêts'; }catch(e){ console.log('banner update error',e); } } // Hook into renderAll to update banner function translateCategory(cat, targetLang){ if(!cat) return cat; const map={ 'alimentation': {fr:'Alimentation', en:'Food', ar:'مواد غذائية'}, 'مواد غذائية': {fr:'Alimentation', en:'Food', ar:'مواد غذائية'}, 'food': {fr:'Alimentation', en:'Food', ar:'مواد غذائية'}, 'transport': {fr:'Transport', en:'Transport', ar:'نقل'}, 'نقل': {fr:'Transport', en:'Transport', ar:'نقل'}, 'loyer': {fr:'Loyer', en:'Rent', ar:'كراء'}, 'كراء': {fr:'Loyer', en:'Rent', ar:'كراء'}, 'rent': {fr:'Loyer', en:'Rent', ar:'كراء'}, 'factures': {fr:'Factures', en:'Bills', ar:'فواتير'}, 'فواتير': {fr:'Factures', en:'Bills', ar:'فواتير'}, 'bills': {fr:'Factures', en:'Bills', ar:'فواتير'}, 'électricité': {fr:'Électricité', en:'Electricity', ar:'كهرباء'}, 'electricity': {fr:'Électricité', en:'Electricity', ar:'كهرباء'}, 'كهرباء': {fr:'Électricité', en:'Electricity', ar:'كهرباء'}, 'salaire': {fr:'Salaire', en:'Salary', ar:'راتب'}, 'salary': {fr:'Salaire', en:'Salary', ar:'راتب'}, 'راتب': {fr:'Salaire', en:'Salary', ar:'راتب'}, 'soins': {fr:'Soins', en:'Healthcare', ar:'علاج'}, 'علاج': {fr:'Soins', en:'Healthcare', ar:'علاج'}, 'autre': {fr:'Autre', en:'Other', ar:'أخرى'}, 'أخرى': {fr:'Autre', en:'Other', ar:'أخرى'}, 'other': {fr:'Autre', en:'Other', ar:'أخرى'}, }; const key=cat.toLowerCase().trim(); if(map[key]) return map[key][targetLang]||cat; for(const k in map){ if(key.includes(k) || k.includes(key)) return map[k][targetLang]||cat; } return cat; } function openAddVente(){ openAdd('Rentrée'); setTimeout(()=>{ const d=DICT[lang]; const catSel=document.getElementById('catTx'); if(catSel){ const target=d.catVente||'Ventes'; for(let i=0;i{ const d=DICT[lang]; const catSel=document.getElementById('catTx'); if(catSel){ const target=d.catPret||'Prêts'; for(let i=0;i{ if(c.id!=='containerSecretBanner'){ c.classList.remove('open'); c.style.display='none'; }}); if(!isOpen){ cont.style.display='block'; cont.classList.add('open'); if(arrow) arrow.textContent='⌄'; loadSecretSingle(); cont.animate([{transform:'translateY(-15px)',opacity:0},{transform:'translateY(0)',opacity:1}],{duration:350,easing:'ease-out'}); } else { if(arrow) arrow.textContent='›'; cont.style.display='none'; cont.classList.remove('open'); } } function refreshSecretQuestionsOptions(){ const d=DICT[lang]; const sel=document.getElementById('secretQSingle'); if(!sel) return; const current=sel.value; const map=[['animal',d.secretQ_animal],['mere',d.secretQ_mere],['ecole',d.secretQ_ecole],['plat',d.secretQ_plat],['surnom',d.secretQ_surnom],['voiture',d.secretQ_voiture],['film',d.secretQ_film],['pere',d.secretQ_pere],['couleur',d.secretQ_couleur],['amis',d.secretQ_amis]]; sel.innerHTML=''+map.map(m=>``).join(''); if(current) sel.value=current; } function loadSecretSingle(){ refreshSecretQuestionsOptions(); const q=localStorage.getItem('ghorab_secret_q_single'); const a=localStorage.getItem('ghorab_secret_a_single'); if(q){ const el=document.getElementById('secretQSingle'); if(el) el.value=q; } if(a){ const el=document.getElementById('secretASingle'); if(el) el.value=a; } updateSecretBadge(); } function updateSecretBadge(){ const d=DICT[lang]; const q=localStorage.getItem('ghorab_secret_q_single'); const a=localStorage.getItem('ghorab_secret_a_single'); const badge=document.getElementById('badgeSecretQ'); if(q && a){ if(badge){ badge.textContent='✅ '+q; badge.style.color='#10b981'; } } else { if(badge){ badge.textContent=d.secretNotConfigured; badge.style.color='#f59e0b'; } } } function saveSecretSingle(){ const d=DICT[lang]; const q=document.getElementById('secretQSingle').value; const a=document.getElementById('secretASingle').value.trim(); if(!q){ showToast('❌ '+d.secretChooseOpt); return; } if(!a){ showToast('❌ '+d.secretAnswerPh); return; } localStorage.setItem('ghorab_secret_q_single', q); localStorage.setItem('ghorab_secret_a_single', a.toLowerCase()); updateSecretBadge(); document.getElementById('secretSingleStatus').innerHTML=d.secretSaved; showToast(d.secretSaved); setTimeout(()=>{ toggleSecretBanner(); }, 800); } function clearSecretSingle(){ const d=DICT[lang]; if(!confirm(d.secretDeleted+' ?')) return; localStorage.removeItem('ghorab_secret_q_single'); localStorage.removeItem('ghorab_secret_a_single'); document.getElementById('secretQSingle').value=''; document.getElementById('secretASingle').value=''; updateSecretBadge(); document.getElementById('secretSingleStatus').innerHTML=d.secretDeleted; showToast(d.secretDeleted); } function recoverPass(){ const d=DICT[lang]; const q=localStorage.getItem('ghorab_secret_q_single'); const a=localStorage.getItem('ghorab_secret_a_single'); if(q && a){ const qMap={animal:d.secretQ_animal,mere:d.secretQ_mere,ecole:d.secretQ_ecole,plat:d.secretQ_plat,surnom:d.secretQ_surnom,voiture:d.secretQ_voiture,film:d.secretQ_film,pere:d.secretQ_pere,couleur:d.secretQ_couleur,amis:d.secretQ_amis}; const html='
'+d.secretRecoverTitle+'
'+ (qMap[q]||q) +'
'; let bg=document.getElementById('customModalBg'); if(!bg){ bg=document.createElement('div'); bg.id='customModalBg'; bg.className='modal-bg open'; bg.style.cssText='position:fixed;inset:0;background:rgba(0,0,0,.9);display:grid;place-items:center;z-index:10000;padding:12px'; document.body.appendChild(bg); } bg.innerHTML=html; bg.classList.add('open'); bg.style.display='grid'; } else { const email=prompt(lang==='ar'?'أدخل بريدك:': lang==='en'?'Enter email (no secret question):' :'Aucune question secrète configurée. Entrez email:'); if(email) alert((lang==='ar'?'إرسال إلى: ': lang==='en'?'Send to: ':'Envoi à: ')+email); } } function verifySingleAnswer(){ const d=DICT[lang]; const stored=localStorage.getItem('ghorab_secret_a_single'); const input=document.getElementById('recSingle')?.value.trim().toLowerCase(); if(stored && input===stored){ const pwd=localStorage.getItem('ghorab_app_password'); alert(d.secretCorrect+'\n'+ (lang==='ar'?'كلمة المرور: ': lang==='en'?'Password: ':'Mot de passe: ')+(pwd||'')); closeModal(); document.getElementById('passwordScreen').style.display='none'; } else { alert(d.secretIncorrect); } } function closeModal(){ const bg=document.getElementById('customModalBg'); if(bg){ bg.classList.remove('open'); bg.style.display='none'; } } function savePassword(){const p1=document.getElementById('newPass').value;const p2=document.getElementById('confirmPass').value;if(!p1||p1!==p2){alert(lang==='ar'?'غير متطابق':'Non identique');return}localStorage.setItem('ghorab_app_password',p1);document.getElementById('passStatus').innerHTML='✓ OK';renderAll();} function removePassword(){if(!confirm(lang==='ar'?'حذف ؟':'Supprimer ?'))return;localStorage.removeItem('ghorab_app_password');document.getElementById('passStatus').innerHTML='Supprimé';renderAll();} function connectDropbox(){ const email = document.getElementById('inputDropboxParam')?.value || localStorage.getItem('ghorab_dropbox_email') || ''; if(!email){ showToast('⚠️ Ajoute ton email Dropbox d\'abord'); return; } localStorage.setItem('ghorab_dropbox_connected','true'); localStorage.setItem('ghorab_dropbox_email', email); const st=document.getElementById('dropStatus'); if(st) st.innerHTML='✓ Dropbox connecté
'+email+'
'+new Date().toLocaleString()+''; const badge=document.getElementById('badgeDropboxParam'); if(badge) badge.innerHTML=''; showToast('✓ Dropbox connecté: '+email); } function connectGDrive(){ const email = document.getElementById('inputGdriveParam')?.value || localStorage.getItem('ghorab_gdrive_email') || ''; if(!email){ showToast('⚠️ Ajoute ton email Google Drive d\'abord'); return; } localStorage.setItem('ghorab_gdrive_connected','true'); localStorage.setItem('ghorab_gdrive_email', email); const st=document.getElementById('gdriveStatus'); if(st) st.innerHTML='✓ Google Drive connecté
'+email+'
'+new Date().toLocaleString()+''; const badge=document.getElementById('badgeGdriveParam'); if(badge) badge.innerHTML=''; showToast('✓ Google Drive connecté: '+email); } function backupToGDrive(){ const data = {transactions, comptes, rates, budget, date: new Date().toISOString(), email: localStorage.getItem('ghorab_gdrive_email')||''}; localStorage.setItem('ghorab_gdrive_backup', JSON.stringify(data)); localStorage.setItem('ghorab_dropbox_backup', JSON.stringify(data)); const st=document.getElementById('gdriveStatus'); if(st) st.innerHTML='✓ Backup OK: '+new Date().toLocaleString()+'
'+transactions.length+' transactions'; const st2=document.getElementById('dropStatus'); if(st2) st2.innerHTML='✓ Backup OK: '+new Date().toLocaleString()+'
'+transactions.length+' transactions'; showToast('☁️ Backup local OK ('+transactions.length+')'); // Propose download const blob = new Blob([JSON.stringify(data,null,2)], {type:'application/json'}); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href=url; a.download='Ghorab_Pro_Backup_'+new Date().toISOString().slice(0,10)+'.json'; // auto download optional // a.click(); } function restoreFromGDrive(){ const sources = ['ghorab_gdrive_backup','ghorab_dropbox_backup','ghorab_pro_v4']; let found=null, srcName=''; for(const s of sources){ const b=localStorage.getItem(s); if(b){ try{ const d=JSON.parse(b); if(d.transactions || Array.isArray(d)){ found=d; srcName=s; break; } }catch(e){} } } if(!found){ showToast('❌ Aucun backup trouvé'); return; } try{ if(found.transactions){ transactions=found.transactions; comptes=found.comptes||comptes; rates=found.rates||rates; } else if(Array.isArray(found)){ transactions=found; } localStorage.setItem('ghorab_pro_v4', JSON.stringify(transactions)); if(found.comptes) localStorage.setItem('ghorab_comptes', JSON.stringify(found.comptes)); renderAll(); showToast('📥 Restauré depuis '+srcName+' ('+transactions.length+')'); const st=document.getElementById('gdriveStatus'); if(st) st.innerHTML='✓ Restauré: '+new Date().toLocaleString()+' depuis '+srcName; }catch(e){ showToast('❌ Erreur restauration: '+e.message); } } function checkCloudStatus(){ try{ const dConn=localStorage.getItem('ghorab_dropbox_connected'); const gConn=localStorage.getItem('ghorab_gdrive_connected'); const dEmail=localStorage.getItem('ghorab_dropbox_email')||''; const gEmail=localStorage.getItem('ghorab_gdrive_email')||''; const dSt=document.getElementById('dropStatus'); const gSt=document.getElementById('gdriveStatus'); if(dConn && dSt) dSt.innerHTML='✓ Connecté: '+dEmail; if(gConn && gSt) gSt.innerHTML='✓ Connecté: '+gEmail; const bd=document.getElementById('badgeDropboxParam'); const bg=document.getElementById('badgeGdriveParam'); if(dConn && bd) bd.innerHTML=''; if(gConn && bg) bg.innerHTML=''; }catch(e){} } setTimeout(checkCloudStatus, 1000); function requestCamera(){ showToast('📷 Ouverture appareil photo...'); openCameraForReceipt(); const st=document.getElementById('cameraStatus'); if(st) st.innerHTML='📷 Ouverture caméra native...
Prends la photo du reçu'; const badge=document.getElementById('badgeCameraParam'); if(badge) badge.innerHTML=''; } function checkCameraStatus(){ renderReceiptsGallery(); const st=document.getElementById('cameraStatus'); const count = getReceipts().length; if(count>0){ if(st) st.innerHTML='✅ '+count+' reçu(s) stocké(s)
En mémoire locale'; } else { if(st) st.innerHTML='✅ Prêt - Clique pour prendre une photo
Aucune autorisation nécessaire - Stockage local'; } const badge=document.getElementById('badgeCameraParam'); if(badge) badge.innerHTML=''+count+''; } function openCameraForReceipt(){ const input=document.getElementById('receiptCameraInput'); if(input){ input.value=''; input.click(); } } function getReceipts(){ try{ return JSON.parse(localStorage.getItem('ghorab_receipts_names')||'[]'); }catch(e){ return []; } } function saveReceipts(arr){ try{ localStorage.setItem('ghorab_receipts_names', JSON.stringify(arr)); }catch(e){} renderReceiptsGallery(); checkCameraStatus(); } function checkCameraStatus(){ try{ localStorage.removeItem('ghorab_receipts'); localStorage.removeItem('ghorab_last_receipt'); }catch(e){} const st=document.getElementById('cameraStatus'); try{ const req=indexedDB.open('GhorabReceipts',1); req.onupgradeneeded=e=>e.target.result.createObjectStore('receipts',{keyPath:'id'}); req.onsuccess=e=>{ const db=e.target.result; const tx=db.transaction('receipts','readonly'); const all=tx.objectStore('receipts').getAll(); all.onsuccess=()=>{ const list=all.result||[]; const count=list.length; const total=list.reduce((a,b)=>a+(b.size||0),0); if(st){ if(count>0) st.innerHTML='✅ '+count+' reçu(s) - '+total+' Ko
Illimité + Galerie'; else st.innerHTML='✅ Prêt - Clique pour photo
50+ photos illimité'; } const badge=document.getElementById('badgeCameraParam'); if(badge) badge.innerHTML=''+count+''; }; }; }catch(e){} renderReceiptsGallery(); } function openCameraForReceipt(){ const inp=document.getElementById('receiptCameraInput'); if(inp){ inp.value=''; inp.click(); return; } const tmp=document.createElement('input'); tmp.type='file'; tmp.accept='image/*'; tmp.setAttribute('capture','environment'); tmp.onchange=function(){ handleReceiptPhoto(this); }; tmp.click(); } function handleReceiptPhoto(input){ if(input.files && input.files[0]){ const file=input.files[0]; const st=document.getElementById('cameraStatus'); if(st) st.innerHTML='⏳ Compression...'; const reader=new FileReader(); reader.onload=function(e){ const img=new Image(); img.onload=function(){ let w=img.width,h=img.height; const maxW=800; if(w>maxW){ h=h*maxW/w; w=maxW; } const canvas=document.createElement('canvas'); canvas.width=w; canvas.height=h; canvas.getContext('2d').drawImage(img,0,0,w,h); const comp=canvas.toDataURL('image/jpeg',0.6); const compSize=Math.round(comp.length/1024); // Stocke image dans variable globale pour transaction (mais pas dans localStorage transaction) window._attachedReceiptData = comp; window._attachedReceiptName = file.name||('recu_'+Date.now()+'.jpg'); localStorage.setItem('ghorab_last_receipt_name', window._attachedReceiptName); // Ne PAS stocker comp dans ghorab_last_receipt (trop gros) - on le garde en mémoire try{ localStorage.removeItem('ghorab_last_receipt'); }catch(e){} // IndexedDB try{ const req=indexedDB.open('GhorabReceipts',1); req.onupgradeneeded=ev=>ev.target.result.createObjectStore('receipts',{keyPath:'id'}); req.onsuccess=ev=>{ const db=ev.target.result; const tx=db.transaction('receipts','readwrite'); tx.objectStore('receipts').add({id:Date.now(), data:comp, name:window._attachedReceiptName, date:new Date().toLocaleString(), size:compSize, origSize:Math.round(file.size/1024)}); tx.oncomplete=()=>{ renderReceiptsGallery(); checkCameraStatus(); }; }; }catch(err){} // Galerie try{ const url=URL.createObjectURL(file); const a=document.createElement('a'); a.href=url; a.download='Ghorab_Recu_'+Date.now()+'.jpg'; document.body.appendChild(a); a.click(); setTimeout(()=>{document.body.removeChild(a); URL.revokeObjectURL(url);},1500); }catch(e){} const preview=document.getElementById('receiptPreview'); if(preview){ preview.src=comp; preview.style.display='block'; } if(st) st.innerHTML='✅ Photo prête
'+window._attachedReceiptName+'
'+Math.round(file.size/1024)+' Ko → '+compSize+' Ko
Ajoute maintenant ta dépense - elle sera liée'; if(typeof showToast==='function') showToast('📸 Reçu prêt - Ajoute dépense maintenant'); const names=JSON.parse(localStorage.getItem('ghorab_receipts_names')||'[]'); names.push({id:Date.now(), name:window._attachedReceiptName, size:compSize, date:new Date().toLocaleString()}); localStorage.setItem('ghorab_receipts_names', JSON.stringify(names)); // Affiche zone d'attache dans modal const zone=document.getElementById('receiptAttachZone'); if(zone) zone.style.display='block'; const imgPrev=document.getElementById('attachedReceiptPreview'); if(imgPrev){ imgPrev.src=comp; imgPrev.style.display='block'; } const nameEl=document.getElementById('attachedReceiptName'); if(nameEl) nameEl.textContent=window._attachedReceiptName; }; img.src=e.target.result; }; reader.readAsDataURL(file); } } function renderReceiptsGallery(){ const gallery=document.getElementById('receiptsGallery'); const countEl=document.getElementById('receiptCount'); if(!gallery) return; try{ const req=indexedDB.open('GhorabReceipts',1); req.onupgradeneeded=e=>e.target.result.createObjectStore('receipts',{keyPath:'id'}); req.onsuccess=e=>{ const db=e.target.result; const tx=db.transaction('receipts','readonly'); const all=tx.objectStore('receipts').getAll(); all.onsuccess=()=>{ const receipts=all.result||[]; if(countEl) countEl.textContent=receipts.length; if(receipts.length===0){ gallery.innerHTML='
Aucun reçu
Illimité
'; return; } gallery.innerHTML=receipts.map(r=>`
${r.name}
${r.date} - ${r.size} Ko
`).join(''); }; }; }catch(e){} } function deleteReceipt(id){ try{ const req=indexedDB.open('GhorabReceipts',1); req.onsuccess=e=>{ const db=e.target.result; const tx=db.transaction('receipts','readwrite'); tx.objectStore('receipts').delete(id); tx.oncomplete=()=>{ renderReceiptsGallery(); checkCameraStatus(); }; }; }catch(e){} } function shareReceipt(id){ try{ const req=indexedDB.open('GhorabReceipts',1); req.onsuccess=e=>{ const db=e.target.result; const tx=db.transaction('receipts','readonly'); const get=tx.objectStore('receipts').get(id); get.onsuccess=()=>{ const r=get.result; if(!r) return; const win=window.open(); win.document.write(''); }; }; }catch(e){} } function resetCameraPermission(){ if(confirm('Supprimer tous les reçus?')){ try{ const req=indexedDB.open('GhorabReceipts',1); req.onsuccess=e=>{ const db=e.target.result; const tx=db.transaction('receipts','readwrite'); tx.objectStore('receipts').clear(); tx.oncomplete=()=>{ localStorage.removeItem('ghorab_receipts'); localStorage.removeItem('ghorab_receipts_names'); renderReceiptsGallery(); checkCameraStatus(); }; }; }catch(e){} } } function exportReceipts(){ try{ const req=indexedDB.open('GhorabReceipts',1); req.onsuccess=e=>{ const db=e.target.result; const tx=db.transaction('receipts','readonly'); const all=tx.objectStore('receipts').getAll(); all.onsuccess=()=>{ const receipts=all.result||[]; if(receipts.length===0) return; const blob=new Blob([JSON.stringify(receipts)],{type:'application/json'}); const url=URL.createObjectURL(blob); const a=document.createElement('a'); a.href=url; a.download='Ghorab_Recus.json'; a.click(); }; }; }catch(e){} } setTimeout(checkCameraStatus, 800); function renderReceiptsGallery(){ const gallery=document.getElementById('receiptsGallery'); if(!gallery) return; const receipts=getReceipts(); if(receipts.length===0){ gallery.innerHTML='
Aucun reçu stocké
'; return; } gallery.innerHTML=receipts.map(r=>`
${r.name}
${r.date} - ${r.size} Ko
Stocké localement
`).join(''); } function deleteReceipt(id){ if(!confirm('Supprimer ce reçu?')) return; let receipts=getReceipts(); receipts=receipts.filter(r=>r.id!==id); saveReceipts(receipts); showToast('🗑 Reçu supprimé'); } function shareReceipt(id){ const receipts=getReceipts(); const r=receipts.find(x=>x.id===id); if(!r) return; const win=window.open(); win.document.write(''); } function resetCameraPermission(){ if(confirm('Supprimer TOUS les reçus stockés?')){ localStorage.removeItem('ghorab_receipts'); localStorage.removeItem('ghorab_last_receipt'); renderReceiptsGallery(); checkCameraStatus(); showToast('🗑 Tous les reçus supprimés'); } } function exportReceipts(){ const receipts=getReceipts(); if(receipts.length===0){ showToast('Aucun reçu'); return; } const blob=new Blob([JSON.stringify(receipts)], {type:'application/json'}); const url=URL.createObjectURL(blob); const a=document.createElement('a'); a.href=url; a.download='Ghorab_Recus_'+new Date().toISOString().slice(0,10)+'.json'; a.click(); showToast('💾 Export '+receipts.length+' reçus'); } setTimeout(checkCameraStatus, 500); let _attachedReceiptData = null; let _attachedReceiptName = ''; function handleAttachReceipt(input){ if(input.files && input.files[0]){ const file=input.files[0]; const reader=new FileReader(); reader.onload=function(e){ window._attachedReceiptData = e.target.result; window._attachedReceiptName = file.name; const zone=document.getElementById('receiptAttachZone'); const img=document.getElementById('attachedReceiptPreview'); const nameEl=document.getElementById('attachedReceiptName'); const btnView=document.getElementById('btnViewLastReceipt'); if(zone) zone.style.display='block'; if(img){ img.src=e.target.result; img.style.display='block'; } if(nameEl) nameEl.textContent=file.name+' ('+Math.round(file.size/1024)+' Ko)'; if(btnView) btnView.style.display='block'; showToast('📸 Reçu attaché à cette dépense'); // Also save to global receipts list const receipts = JSON.parse(localStorage.getItem('ghorab_receipts')||'[]'); receipts.push({id:Date.now(), data:e.target.result, name:file.name, date:new Date().toLocaleString(), size:Math.round(file.size/1024)}); localStorage.setItem('ghorab_receipts', JSON.stringify(receipts)); }; reader.readAsDataURL(file); } } function removeAttachedReceipt(){ window._attachedReceiptData = null; window._attachedReceiptName = ''; localStorage.removeItem('ghorab_last_receipt'); localStorage.removeItem('ghorab_last_receipt_name'); const zone=document.getElementById('receiptAttachZone'); if(zone) zone.style.display='none'; showToast('Reçu détaché'); } function viewAttachedReceipt(){ const data = window._attachedReceiptData || localStorage.getItem('ghorab_last_receipt'); if(!data){ showToast('Aucun reçu'); return; } const win=window.open(); win.document.write('

'+(window._attachedReceiptName||'')+'

'); } // Auto attach last receipt when opening add modal const _origOpenAdd = openAdd; function openAdd(type){ _origOpenAdd(type); // Check if there's a last receipt setTimeout(()=>{ const last = localStorage.getItem('ghorab_last_receipt'); const lastName = localStorage.getItem('ghorab_last_receipt_name'); if(last && !editingId){ window._attachedReceiptData = last; window._attachedReceiptName = lastName; const zone=document.getElementById('receiptAttachZone'); const img=document.getElementById('attachedReceiptPreview'); const nameEl=document.getElementById('attachedReceiptName'); const btnView=document.getElementById('btnViewLastReceipt'); if(zone) zone.style.display='block'; if(img){ img.src=last; img.style.display='block'; } if(nameEl) nameEl.textContent=lastName+' (auto)'; if(btnView) btnView.style.display='block'; } else { const zone=document.getElementById('receiptAttachZone'); if(zone && !editingId) zone.style.display='none'; } }, 200); } function getFiltered(){const sel=document.getElementById('compteTop').value;let f=transactions;if(sel!=='all')f=f.filter(x=>x.compte===sel);return f;} function ensureDefaultCompte(){ if(!comptes || comptes.length===0 || comptes.includes("Caisse") || comptes.includes("Banque") || comptes.includes("CCP")){ comptes=["Carte 1","Espèces","Banque CCP","Prêt particulier"]; localStorage.setItem("ghorab_comptes",JSON.stringify(comptes)); } // Ne plus toucher au DOM ici - renderAll s'en charge et préserve la sélection } function renderAll(){ // FIX BUG CARTE BLOQUEE: on s'assure des comptes AVANT de lire les selects, sans setTimeout ensureDefaultCompte(); try{ const elInfo=document.getElementById('comptesInfo'); if(elInfo){ if(comptes.length===0 || (comptes.length===1 && comptes[0]==="Principal")){ elInfo.textContent=''; const parent=elInfo.closest('div'); if(parent) parent.style.display='none'; } else { elInfo.textContent=comptes.join(', '); const parent=elInfo.closest('div'); if(parent) parent.style.display='block'; } } localStorage.setItem('ghorab_comptes',JSON.stringify(comptes)); updatePurgeBtn(); }catch(e){} // Sauvegarde sélection AVANT reconstruction const topEl=document.getElementById('compteTop'); const curTopSaved=topEl ? topEl.value : 'all'; const deviseEl=document.getElementById('deviseTop'); const disp=deviseEl ? deviseEl.value : (localStorage.getItem('ghorab_display_devise')||'EUR'); localStorage.setItem('ghorab_display_devise',disp); document.getElementById('deviseBadge').textContent=fmtCurr(disp); document.querySelectorAll('.currLab').forEach(e=>e.textContent=fmtCurr(disp)); document.querySelectorAll('.currLab2').forEach(e=>e.textContent=fmtCurr(disp)); document.getElementById('currIn').textContent=fmtCurr(disp); const filtered=getFiltered(); // Calcul corrigé avec type auto let totalIn=0,totalOut=0; filtered.forEach(x=>{ const corrType=x.type; // FIX v6.3: utilise type stocké pas catégorie const c=convert(x.montant,x.devise||'EUR',disp); if(corrType==='Rentrée') totalIn+=c; else totalOut+=c; }); document.getElementById('soldeM').textContent=(totalIn-totalOut).toFixed(2)+' '+fmtCurr(disp); const top=document.getElementById('compteTop'); const d=DICT[lang]; const optsHtml=''+comptes.map(c=>{ let label=c;if(c==='Caisse')label=d.caisse;if(c==='Banque')label=d.banque;if(c==='CCP')label=d.ccp; return ``; }).join(''); if(top){ top.innerHTML=optsHtml; // Restaure la sélection sauvegardée if(comptes.includes(curTopSaved)||curTopSaved==='all'){ top.value=curTopSaved; } else { top.value='all'; } } document.getElementById('compteSel').innerHTML=comptes.map(c=>``).join(''); let inM=0,outM=0; filtered.forEach(x=>{ const corrType=x.type; // FIX v6.3: utilise type stocké pas catégorie const c=convert(x.montant,x.devise||'EUR',disp); if(corrType==='Rentrée') inM+=c; else outM+=c; }); document.getElementById('inM').textContent=inM.toFixed(2); document.getElementById('outM').textContent=outM.toFixed(2); document.getElementById('prevM').textContent=(0).toFixed(2); const monthsAr=['جانفي','فيفري','مارس','أفريل','ماي','جوان','جويلية','أوت','سبتمبر','أكتوبر','نوفمبر','ديسمبر']; const monthsFr=['Janvier','Février','Mars','Avril','Mai','Juin','Juillet','Août','Septembre','Octobre','Novembre','Décembre']; const months=lang==='ar'?monthsAr:monthsFr; document.getElementById('monthLab').textContent=months[new Date().getMonth()]+' + '+(lang==='ar'?'الباقي':'Restes'); document.getElementById('countLab').textContent=filtered.length; const breakdown={};filtered.forEach(x=>{const corrType=getCorrectTypeForCat(x.cat);const dev=x.devise||'EUR';if(!breakdown[dev]) breakdown[dev]={in:0,out:0};const c=x.montant;if(corrType==='Rentrée') breakdown[dev].in+=c; else breakdown[dev].out+=c;}); document.getElementById('multiBreakdown').innerHTML=Object.entries(breakdown).map(([k,v])=>`${getCurr(k).flag} ${k}: +${v.in.toFixed(2)} / -${v.out.toFixed(2)}`).join(' | ')||''; renderMouv();renderCat();renderComptes();renderRatesEditor();renderBudget();renderCal();renderFreq(); document.getElementById('licInfo').textContent=localStorage.getItem('ghorab_license_v4')||'—'; // comptesInfo removed updateNotifPermissionUI(); const pwd=localStorage.getItem('ghorab_app_password'); document.getElementById('passStatus').innerHTML=pwd?'✓ '+(lang==='ar'?'مفعلة':'Actif')+'':''+(lang==='ar'?'لا يوجد':'Aucun')+''; } function openAdd(t){ currentType=t;editingId=null;const d=DICT[lang]; document.getElementById('modalAddTitle').textContent=t==='Rentrée'?d.modalAddRev:d.modalAddDep; document.getElementById('montant').value='';document.getElementById('noteTx').value='';document.getElementById('dateTx').valueAsDate=new Date(); document.getElementById('montant').placeholder=d.placeholderAmount; document.getElementById('noteTx').placeholder=d.placeholderNote; document.getElementById('btnCancel').textContent=d.cancel; document.getElementById('btnSave').textContent=d.save; document.getElementById('modalAdd').classList.add('open'); } function closeAdd(){document.getElementById('modalAdd').classList.remove('open');} function saveTx(){ const m=parseFloat(document.getElementById('montant').value); if(!m){alert(DICT[lang].placeholderAmount+' ?');return} const cat=document.getElementById('categorie').value; const devise=document.getElementById('deviseSel').value; const finalType = currentType; // Récupère reçu attaché - MAIS NE PAS STOCKER BASE64 DANS TRANSACTION (cause plein) const attachedReceiptName = window._attachedReceiptName || localStorage.getItem('ghorab_last_receipt_name') || ''; // On ne garde que le nom, pas le data base64 qui fait exploser localStorage const tx={id:editingId||Date.now(),type:finalType,montant:m,devise:devise,cat:cat,date:document.getElementById('dateTx').value,note:document.getElementById('noteTx').value,compte:document.getElementById('compteSel').value, receipt: null, receiptName: attachedReceiptName, hasReceipt: !!(window._attachedReceiptData)}; if(editingId){ const i=transactions.findIndex(x=>x.id===editingId); transactions[i]=tx; }else{ transactions.unshift(tx); } localStorage.setItem('ghorab_pro_v4',JSON.stringify(transactions)); // AUTO BUDGET: quand on ajoute un revenu, budget = total revenus (utilise type stocké) if(finalType==='Rentrée'){ const disp=document.getElementById('deviseTop').value; let totalRev=0; transactions.forEach(t=>{ if(t.type==='Rentrée'){ totalRev+=convert(t.montant,t.devise||'EUR',disp); } }); budget={montant:totalRev, devise:disp}; localStorage.setItem('ghorab_budget_multi',JSON.stringify(budget)); document.getElementById('budgetInput').value=totalRev.toFixed(2); document.getElementById('budgetDevise').value=disp; showToast('🟢 Revenu +'+m+' '+devise+' ('+cat+') → Budget auto = '+totalRev.toFixed(2)+' '+disp); }else{ showToast('🔴 -'+m+' '+devise+' ('+cat+')'); } editingId=null;closeAdd();renderAll(); } function loadReceiptForEdit(tx){ if(tx && tx.receipt){ window._attachedReceiptData = tx.receipt; window._attachedReceiptName = tx.receiptName||'recu.jpg'; const zone=document.getElementById('receiptAttachZone'); const img=document.getElementById('attachedReceiptPreview'); const nameEl=document.getElementById('attachedReceiptName'); const btnView=document.getElementById('btnViewLastReceipt'); setTimeout(()=>{ if(zone) zone.style.display='block'; if(img){ img.src=tx.receipt; img.style.display='block'; } if(nameEl) nameEl.textContent=tx.receiptName||'reçu joint'; if(btnView) btnView.style.display='block'; }, 300); } } function editTx(id){const tx=transactions.find(x=>x.id===id);if(!tx)return;editingId=id;currentType=getCorrectTypeForCat(tx.cat);const d=DICT[lang];document.getElementById('modalAddTitle').textContent=currentType==='Rentrée'?d.modalAddRev:d.modalAddDep;document.getElementById('montant').value=tx.montant;document.getElementById('deviseSel').value=tx.devise||'EUR';document.getElementById('categorie').value=tx.cat;document.getElementById('compteSel').value=tx.compte;document.getElementById('dateTx').value=tx.date;document.getElementById('noteTx').value=tx.note; loadReceiptForEdit(tx);document.getElementById('montant').placeholder=d.placeholderAmount;document.getElementById('noteTx').placeholder=d.placeholderNote;document.getElementById('btnCancel').textContent=d.cancel;document.getElementById('btnSave').textContent=d.save;document.getElementById('modalAdd').classList.add('open');} function delTx(id){if(!confirm(lang==='ar'?'حذف ؟':'Supprimer ?'))return;transactions=transactions.filter(x=>x.id!==id);localStorage.setItem('ghorab_pro_v4',JSON.stringify(transactions));renderAll();} function filterMouv(type){mouvFilter=type;renderMouv();} function renderMouv(){ const q=(document.getElementById('searchMouv')?.value||'').toLowerCase(); let f=getFiltered(); // Filtre type if(mouvFilter!=='all') f=f.filter(x=>getCorrectTypeForCat(x.cat)===mouvFilter); if(q) f=f.filter(x=>(x.cat+x.note+x.compte).toLowerCase().includes(q)); const disp=document.getElementById('deviseTop').value; const d=DICT[lang]; // Total let sum=0; f.forEach(x=>{ const corr=x.type; const c=convert(x.montant,x.devise||'EUR',disp); sum+=corr==='Rentrée'?c:-c; }); document.getElementById('mouvTotal').textContent=sum.toFixed(2)+' '+fmtCurr(disp); document.getElementById('listMouv').innerHTML=f.map(x=>{ const corrType=x.type; // FIX v6.3: utilise type stocké pas catégorie const isIn=corrType==='Rentrée'; const curr=getCurr(x.devise||'EUR'); const conv=convert(x.montant,x.devise||'EUR',disp); const showConv=x.devise!==disp; const sign=isIn?'+':'-'; const color=isIn?'#4ade80':'#f87171'; return `
${x.cat} ${curr.flag} ${curr.code} ${corrType}
${sign}${x.montant} ${curr.symbol} ${showConv?`→ ${sign}${conv.toFixed(2)} ${fmtCurr(disp)}`:''}
${x.date} • ${tCompte(x.compte)} ${x.note?`• ${x.note}`:''}
`; }).join('')||`
${d.noMouv}
`; } function openPage(id){document.getElementById(id).classList.add('open');if(id==='pageDate'){if(!document.getElementById('dateDebut').value){document.getElementById('dateDebut').valueAsDate=new Date(new Date().setDate(1));document.getElementById('dateFin').valueAsDate=new Date();} dateFilterType='all'; renderDate();}if(id==='pageNotif')updateNotifPermissionUI();if(id==='pageParams'){setTimeout(()=>{renderRatesEditor();fetchRealRates();},300);}if(id==='pageMouv'){mouvFilter='all';renderMouv();}} function closePage(id){ try{ var el=document.getElementById(id); if(el){ el.classList.remove('open'); el.style.display='none'; el.style.visibility=''; } }catch(e){} } let dateFilterType='all'; function setDateFilterType(t){dateFilterType=t; renderDate();} function renderDate(){ const d1Val=document.getElementById('dateDebut').value; const d2Val=document.getElementById('dateFin').value; if(!d1Val || !d2Val){showToast('Choisissez 2 dates');return;} const d1=new Date(d1Val),d2=new Date(d2Val); d2.setHours(23,59,59,999); const disp=document.getElementById('deviseTop').value; let f=getFiltered().filter(x=>{const d=new Date(x.date);return d>=d1&&d<=d2}); // Filtre type let fType=f; if(dateFilterType!=='all'){ fType=f.filter(x=>getCorrectTypeForCat(x.cat)===dateFilterType); } let inT=0,outT=0; f.forEach(x=>{ const corr=x.type; const c=convert(x.montant,x.devise||'EUR',disp); if(corr==='Rentrée') inT+=c; else outT+=c; }); let inTFilter=0,outTFilter=0; fType.forEach(x=>{ const corr=x.type; const c=convert(x.montant,x.devise||'EUR',disp); if(corr==='Rentrée') inTFilter+=c; else outTFilter+=c; }); let html=`
${f.length} opérations totales${d1Val} → ${d2Val}
Revenus: +${inT.toFixed(2)} ${fmtCurr(disp)} (${f.filter(x=>x.type==='Rentrée').length} op)
Dépenses: -${outT.toFixed(2)} ${fmtCurr(disp)} (${f.filter(x=>x.type==='Dépense').length} op)
Solde total: ${(inT-outT).toFixed(2)} ${fmtCurr(disp)}
`; html+=`
`; if(fType.length===0){ html+=`
Aucune opération pour filtre "${dateFilterType}"
`; }else{ html+=`
Filtre: ${dateFilterType==='all'?'Tous':dateFilterType} → ${fType.length} op • Revenus +${inTFilter.toFixed(2)} • Dépenses -${outTFilter.toFixed(2)} • Solde ${(inTFilter-outTFilter).toFixed(2)}
`; html+=fType.map(x=>{ const corr=getCorrectTypeForCat(x.cat); const isIn=corr==='Rentrée'; const curr=getCurr(x.devise||'EUR'); const conv=convert(x.montant,x.devise||'EUR',disp); const color=isIn?'#4ade80':'#f87171'; return `
${x.cat} ${curr.flag} ${x.devise||'EUR'} ${corr}
${x.date} • ${tCompte(x.compte)} ${x.note?`• ${x.note}`:''}
${isIn?'+':'-'}${x.montant} ${curr.symbol}
${conv.toFixed(2)} ${fmtCurr(disp)}
`; }).join(''); } document.getElementById('resDate').innerHTML=html; } function renderCat(){ const disp=document.getElementById('deviseTop').value; const f=getFiltered(); const grouped={}; f.forEach(t=>{ const corr=t.type; if(corr!=='Dépense') return; const c=convert(t.montant,t.devise||'EUR',disp); grouped[t.cat]=(grouped[t.cat]||0)+c; }); const total=Object.values(grouped).reduce((a,b)=>a+b,0)||1; const d=DICT[lang]; document.getElementById('resCat').innerHTML=Object.entries(grouped).map(([k,v])=>`
${k}
${v.toFixed(2)} ${fmtCurr(disp)} - ${Math.round(v/total*100)}%
`).join('')||`
${d.noDep}
`; } function renderCal(){const year=calMonth.getFullYear(),month=calMonth.getMonth();const monthsAr=['جانفي','فيفري','مارس','أفريل','ماي','جوان','جويلية','أوت','سبتمبر','أكتوبر','نوفمبر','ديسمبر'];const monthsFr=['Janvier','Février','Mars','Avril','Mai','Juin','Juillet','Août','Septembre','Octobre','Novembre','Décembre'];document.getElementById('calTitle').textContent=(lang==='ar'?monthsAr:monthsFr)[month]+' '+year;const first=new Date(year,month,1).getDay(),days=new Date(year,month+1,0).getDate();const grid=document.getElementById('calGrid');grid.innerHTML='';for(let i=0;i';for(let d=1;d<=days;d++){const dateStr=`${year}-${String(month+1).padStart(2,'0')}-${String(d).padStart(2,'0')}`;const has=getFiltered().some(x=>x.date===dateStr);const isToday=new Date().toISOString().slice(0,10)===dateStr;grid.innerHTML+=`
${d}
`;}} function showDay(dateStr){const list=getFiltered().filter(x=>x.date===dateStr);document.getElementById('calDayList').innerHTML=`${dateStr} - ${list.length}`+list.map(x=>{const corr=x.type;return `
${x.cat} - ${corr==='Rentrée'?'+':'-'}${x.montant} ${getCurr(x.devise||'EUR').flag}
`;}).join('');} function changeMonth(dir){calMonth.setMonth(calMonth.getMonth()+dir);renderCal();} function deleteCompte(name){ // v7.0: on autorise tout supprimer const ops=transactions.filter(x=>x.compte===name).length; if(ops>0){ // Modal custom pour compte avec ops const modalHtml = ``; document.body.insertAdjacentHTML('beforeend', modalHtml); return; } // 0 op suppression directe comptes=comptes.filter(c=>c!==name); localStorage.setItem('ghorab_comptes',JSON.stringify(comptes)); if(document.getElementById('compteTop') && document.getElementById('compteTop').value===name){ document.getElementById('compteTop').value='all'; localStorage.setItem('ghorab_display_compte','all'); } renderAll(); showToast('✅ Compte "'+name+'" supprimé'); } function purgeAllComptes(){ const modalHtml = ``; document.body.insertAdjacentHTML('beforeend', modalHtml); } function confirmPurgeComptes(){ const m=document.getElementById('purgeComptesModal'); if(m) m.remove(); comptes=[]; localStorage.setItem('ghorab_comptes',JSON.stringify(comptes)); try{let tx=JSON.parse(localStorage.getItem('ghorab_pro_v4')||'[]'); tx.forEach(t=>{ if(["Carte 1","Espèces","Banque CCP","Prêt particulier"].includes(t.compte)) t.compte=''; }); localStorage.setItem('ghorab_pro_v4',JSON.stringify(tx)); transactions=tx;}catch(e){} renderAll(); showToast('✅ Caisse/Banque/CCP supprimés'); setTimeout(()=>{ const b=document.getElementById('btnPurgeComptes'); if(b) b.style.display='none'; },500); } function confirmDeleteCompte(name){ const m=document.getElementById('delCompteModal'); if(m) m.remove(); // Déplace les ops vers "Sans compte" transactions.forEach(t=>{ if(t.compte===name) t.compte=''; }); localStorage.setItem('ghorab_pro_v4',JSON.stringify(transactions)); comptes=comptes.filter(c=>c!==name); localStorage.setItem('ghorab_comptes',JSON.stringify(comptes)); renderAll(); showToast('✅ "'+name+'" supprimé - '+transactions.filter(t=>t.compte==='').length+' ops sans compte'); } function deleteCompteForce(name){ comptes=comptes.filter(c=>c!==name); localStorage.setItem('ghorab_comptes',JSON.stringify(comptes)); renderAll(); showToast('🗑️ Supprimé forcé: '+name); } function renderComptes(){ const disp=document.getElementById('deviseTop').value; if(comptes.length===0){ document.getElementById('resComptes').innerHTML=`
💳
Aucun compte
Ajoutez votre propre compte ci-dessous
Ex: Mon Caisse, Banque BNA, etc.
`; return; } document.getElementById('resComptes').innerHTML=comptes.map(c=>{ const allT=transactions.filter(x=>x.compte===c); let s=0; allT.forEach(x=>{ const corr=getCorrectTypeForCat(x.cat); const conv=convert(x.montant,x.devise||'EUR',disp); s+=corr==='Rentrée'?conv:-conv; }); const isEmpty=allT.length===0; const safeName = c.replace(/'/g, "\\'"); return `
💳 ${tCompte(c)}
${allT.length} op • ${s.toFixed(2)} ${fmtCurr(disp)}
${isEmpty?'':''}
`; }).join('')+`
💡 Caisse à 0 op = supprimable direct
Si compte avec opérations, il sera supprimé mais opérations gardées (tu peux les réassigner)
`; } function addCompte(){ const n=document.getElementById('newCompteName').value.trim(); if(!n){showToast('❌ Nom vide');return;} if(comptes.includes(n)){showToast('❌ Compte existe déjà');return;} comptes.push(n); localStorage.setItem('ghorab_comptes',JSON.stringify(comptes)); document.getElementById('newCompteName').value=''; renderAll(); showToast('✅ Compte "'+n+'" ajouté'); } function renderBudget(){ const disp=document.getElementById('deviseTop').value; let outM=0, inM=0; transactions.forEach(x=>{ const corr=x.type; const conv=convert(x.montant,x.devise||'EUR',disp); if(corr==='Dépense') outM+=conv; else inM+=conv; }); // Auto-fill budget si vide et on a des revenus if((!budget.montant || budget.montant==0) && inM>0){ budget={montant:inM, devise:disp}; localStorage.setItem('ghorab_budget_multi',JSON.stringify(budget)); } const bDisp=budget.devise||disp; const budgetConv=convert(budget.montant||0,bDisp,disp); const reste=budgetConv-outM; const colorReste=reste>=0?'#4ade80':'#f87171'; document.getElementById('budgetInput').value=budget.montant||''; document.getElementById('budgetDevise').value=bDisp; document.getElementById('resBudget').innerHTML=`
💰 Revenus totaux: ${inM.toFixed(2)} ${fmtCurr(disp)}
🎯 Budget: ${budget.montant||0} ${fmtCurr(bDisp)} = ${budgetConv.toFixed(2)} ${fmtCurr(disp)} ${budget.montant==inM&&inM>0?'AUTO':''}
💸 Dépensé: ${outM.toFixed(2)} ${fmtCurr(disp)}
Reste: ${reste.toFixed(2)} ${fmtCurr(disp)} ${reste<0?'⚠️ Dépassement':''}
💡 Budget se remplit auto quand tu ajoutes un revenu (Salaire)
`; } function saveBudget(){budget={montant:parseFloat(document.getElementById('budgetInput').value)||0,devise:document.getElementById('budgetDevise').value};localStorage.setItem('ghorab_budget_multi',JSON.stringify(budget));renderBudget();} function resetAll(){ if(transactions.length===0 && (budget.montant===0 || !budget.montant)){ showToast('✅ Déjà vide - 0 opération'); return; } // Ouvre fenêtre confirmation custom (pas confirm bloqué) const revCount = transactions.filter(t=>t.type==='Rentrée').length; const depCount = transactions.filter(t=>t.type==='Dépense').length; const disp=document.getElementById('deviseTop').value; let totalRev=0,totalDep=0; transactions.forEach(t=>{ const c=convert(t.montant,t.devise||'EUR',disp); if(t.type==='Rentrée') totalRev+=c; else totalDep+=c; }); document.getElementById('confirmEraseTitle').textContent = lang==='ar' ? 'مسح كل شيء ؟' : 'Effacer tout ?'; document.getElementById('confirmEraseBody').innerHTML = lang==='ar' ? `هل تريد حقا مسح كل العمليات؟
لا يمكن التراجع!` : `Voulez-vous vraiment effacer toutes les opérations ?
Action irréversible !`; document.getElementById('confirmEraseDetails').innerHTML = `
📊 Opérations:${transactions.length}
🟢 Revenus:${revCount} • +${totalRev.toFixed(2)} ${fmtCurr(disp)}
🔴 Dépenses:${depCount} • -${totalDep.toFixed(2)} ${fmtCurr(disp)}
💾 Licence:Gardée ✅
`; document.getElementById('confirmEraseModal').classList.add('open'); } function closeConfirmErase(){ document.getElementById('confirmEraseModal').classList.remove('open'); } function confirmEraseNow(){ closeConfirmErase(); try{ const keepLicense=localStorage.getItem('ghorab_license_v4'); const keepExpire=localStorage.getItem('ghorab_expire_v4'); const keepLang=localStorage.getItem('ghorab_lang'); const keepPass=localStorage.getItem('ghorab_app_password'); const keepNotif=localStorage.getItem('ghorab_notif'); const keepComptes=localStorage.getItem('ghorab_comptes'); localStorage.removeItem('ghorab_pro_v4'); localStorage.removeItem('ghorab_budget_multi'); localStorage.removeItem('ghorab_rates'); localStorage.removeItem('ghorab_display_devise'); localStorage.removeItem('ghorab_display_compte'); localStorage.removeItem('ghorab_last_notif'); if(keepLicense) localStorage.setItem('ghorab_license_v4',keepLicense); if(keepExpire) localStorage.setItem('ghorab_expire_v4',keepExpire); if(keepLang) localStorage.setItem('ghorab_lang',keepLang); if(keepPass) localStorage.setItem('ghorab_app_password',keepPass); if(keepNotif) localStorage.setItem('ghorab_notif',keepNotif); if(keepComptes) localStorage.setItem('ghorab_comptes',keepComptes); transactions=[]; budget={montant:0,devise:'EUR'}; rates=Object.assign({},DEFAULT_RATES_EUR); localStorage.setItem('ghorab_pro_v4',JSON.stringify(transactions)); localStorage.setItem('ghorab_budget_multi',JSON.stringify(budget)); localStorage.setItem('ghorab_rates',JSON.stringify(rates)); renderAll(); showToast('✅ Tout effacé - 0 revenu, 0 dépense - Licence gardée'); }catch(e){ showToast('❌ Erreur: '+e.message); } } function renderFreq(){ const disp=document.getElementById('deviseTop').value; const f=getFiltered(); const d=DICT[lang]; if(f.length===0){ document.getElementById('resFreq').innerHTML=`
🗓️
${d.noMouv}
Ajoutez au moins 2 dépenses avec même catégorie pour voir les fréquentes
💡 Comment ça marche:
• Loyer chaque mois
• Transport chaque jour
• Salaire chaque mois
L'app détecte auto les catégories répétées
`; return; } const grouped={}; f.forEach(t=>{ const key=t.cat+'|'+(t.devise||'EUR'); if(!grouped[key]) grouped[key]={cat:t.cat,devise:t.devise||'EUR',count:0,total:0,last:t.date,lastAmount:t.montant,compte:t.compte,type:t.type}; grouped[key].count++; grouped[key].total+=convert(t.montant,t.devise||'EUR',disp); if(t.date>grouped[key].last){grouped[key].last=t.date; grouped[key].lastAmount=t.montant; grouped[key].compte=t.compte;} }); const list=Object.values(grouped).sort((a,b)=>b.count-a.count); const frequent=list.filter(x=>x.count>=2); const toShow=frequent.length>0?frequent:list; let html=`
⭐ ${frequent.length} catégories fréquentes détectées (utilisées ≥2 fois) sur ${list.length} catégories
`; html+=toShow.map(g=>{ const curr=getCurr(g.devise); const isIn=g.type==='Rentrée'; const color=isIn?'#4ade80':'#f87171'; const avg=(g.total/g.count).toFixed(2); return `
${g.cat}${curr.flag} ${g.devise}${g.count}x
Dernier: ${g.last} • ${g.compte}
Total: ${g.total.toFixed(2)} ${fmtCurr(disp)} • Moy: ${avg} ${fmtCurr(disp)}
`; }).join(''); html+=`
💡 Cliquez sur + montant pour ajouter vite la même dépense
Exemple: Loyer 2000 EUR chaque mois en 1 clic
`; document.getElementById('resFreq').innerHTML=html; } function quickAddFreq(cat,devise,montant,compte){ closePage('pageFreq'); document.getElementById('categorie').value=cat; document.getElementById('deviseSel').value=devise; document.getElementById('montant').value=montant; document.getElementById('compteSel').value=compte; document.getElementById('dateTx').valueAsDate=new Date(); const corr=getCorrectTypeForCat(cat); currentType=corr; const d=DICT[lang]; document.getElementById('modalAddTitle').textContent=corr==='Rentrée'?d.modalAddRev:d.modalAddDep; document.getElementById('modalAdd').classList.add('open'); showToast('⚡ '+cat+' '+montant+' '+devise+' prêt'); } function filterCatFreq(cat){ closePage('pageFreq'); document.getElementById('searchMouv').value=cat; openPage('pageMouv'); setTimeout(()=>renderMouv(),100); showToast('🔍 Filtre: '+cat); } ['key1','key2','key3','key4','key5'].forEach((id,i)=>{const el=document.getElementById(id);el.addEventListener('input',()=>{el.value=el.value.toUpperCase().replace(/[^A-Z0-9]/g,'');if(el.value.length===5&&i<4)document.getElementById(['key1','key2','key3','key4','key5'][i+1]).focus();});}); document.getElementById('passInput').addEventListener('keypress',(e)=>{if(e.key==='Enter')checkPass();}); (function(){ try{ var c=JSON.parse(localStorage.getItem('ghorab_comptes')||'[]'); if(c.includes('Caisse')||c.includes('Banque')||c.includes('CCP')){ localStorage.setItem('ghorab_comptes', JSON.stringify(["Carte 1","Espèces","Banque CCP","Prêt particulier"])); } if(c.length===0){ localStorage.setItem('ghorab_comptes', JSON.stringify(["Carte 1","Espèces","Banque CCP","Prêt particulier"])); } }catch(e){} })(); checkLicense();applyLang();scheduleNotif(); // ===== NOUVEAU: Gestion 2 boutons Email Dropbox & Drive - v8.3 ===== function toggleEmailParam(type){ let contId='', arrowId='', inputId=''; if(type==='dropbox'){ contId='containerDropboxParam'; arrowId='arrowDropboxParam'; inputId='inputDropboxParam'; } else if(type==='gdrive'){ contId='containerGdriveParam'; arrowId='arrowGdriveParam'; inputId='inputGdriveParam'; } else if(type==='camera'){ contId='containerCameraParam'; arrowId='arrowCameraParam'; inputId=''; } else { contId='container'+type.charAt(0).toUpperCase()+type.slice(1)+'Param'; arrowId='arrow'+type.charAt(0).toUpperCase()+type.slice(1)+'Param'; } const cont=document.getElementById(contId); const arrow=document.getElementById(arrowId); if(!cont) return; const isOpen=cont.classList.contains('open'); // Fermer tous document.querySelectorAll('.email-container').forEach(c=>c.classList.remove('open')); document.querySelectorAll('[id^=arrow]').forEach(a=>{if(a.id.includes('Param')) a.textContent='›';}); if(!isOpen){ cont.classList.add('open'); if(arrow) arrow.textContent='⌄'; if(inputId){ setTimeout(()=>{const inp=document.getElementById(inputId); if(inp) inp.focus();},100); } } } function saveEmailParam(type,from){ let inputId=''; if(from==='page'){ inputId=type==='dropbox'?'inputDropboxPage':'inputGdrivePage'; }else{ inputId=type==='dropbox'?'inputDropboxParam':'inputGdriveParam'; } const val=document.getElementById(inputId).value.trim(); if(!val || !val.includes('@') || !val.includes('.')){showToast('❌ Email invalide');return;} localStorage.setItem('ghorab_email_'+type, val); // sync both inputs const ids=[type==='dropbox'?'inputDropboxParam':'inputGdriveParam', type==='dropbox'?'inputDropboxPage':'inputGdrivePage']; ids.forEach(id=>{const el=document.getElementById(id); if(el) el.value=val;}); updateEmailUI(); showToast('✅ Email '+type+' ajouté: '+val); } function removeEmailParam(type){ localStorage.removeItem('ghorab_email_'+type); ['inputDropboxParam','inputDropboxPage'].forEach(id=>{const el=document.getElementById(id); if(el && type==='dropbox') el.value='';}); ['inputGdriveParam','inputGdrivePage'].forEach(id=>{const el=document.getElementById(id); if(el && type==='gdrive') el.value='';}); updateEmailUI(); showToast('🗑️ Email '+type+' supprimé'); } function updateEmailUI(){ const db=localStorage.getItem('ghorab_email_dropbox'); const gd=localStorage.getItem('ghorab_email_gdrive'); const b1=document.getElementById('badgeDropboxParam'); const b2=document.getElementById('badgeGdriveParam'); if(b1) b1.innerHTML=db?`${db}`:''; if(b2) b2.innerHTML=gd?`${gd}`:''; const d1=document.getElementById('delDropboxParam'); const d2=document.getElementById('delGdriveParam'); if(d1) d1.style.display=db?'inline-block':'none'; if(d2) d2.style.display=gd?'inline-block':'none'; const dispDb=document.getElementById('displayDropboxEmail'); const dispGd=document.getElementById('displayGdriveEmail'); if(dispDb) dispDb.innerHTML=db?`✅ Enregistré: ${db} `:'Aucun email enregistré'; if(dispGd) dispGd.innerHTML=gd?`✅ Enregistré: ${gd} `:'Aucun email enregistré'; // sync inputs if(db){['inputDropboxParam','inputDropboxPage'].forEach(id=>{const el=document.getElementById(id); if(el) el.value=db;});} if(gd){['inputGdriveParam','inputGdrivePage'].forEach(id=>{const el=document.getElementById(id); if(el) el.value=gd;});} } // Charger au démarrage setTimeout(updateEmailUI, 500); document.addEventListener('DOMContentLoaded', updateEmailUI); // ================================================================ function openPageSafe(id){ try{ var el=document.getElementById(id); if(el){ el.style.display=''; el.style.visibility=''; el.classList.add('open'); } else console.log('page not found',id); }catch(e){ console.log('openPage error',e); alert('Erreur ouverture '+id+': '+e.message); } } // Remplace openPage par version safe var _origOpenPage = typeof openPage !== 'undefined' ? openPage : null; openPage = function(id){ return openPageSafe(id); }; // AUTO UPDATE DEVISES - Ghorab Pro window.addEventListener('load', function(){ setTimeout(function(){ try{ let lastFetch = localStorage.getItem('ghorab_last_fetch'); let now = Date.now(); // auto fetch if never or >1h if(!lastFetch || (now - parseInt(lastFetch)) > 3600000){ console.log('Auto fetch devises...'); fetchRealRates(); localStorage.setItem('ghorab_last_fetch', now.toString()); } else { // still load cache if exists if(window._realRates) applyRealRatesToApp(); } }catch(e){ console.log(e); } }, 2000); }); // Auto refresh toutes les heures si app reste ouverte setInterval(function(){ try{ fetchRealRates(); localStorage.setItem('ghorab_last_fetch', Date.now().toString()); }catch(e){} }, 3600000);